package jxtend.processor.eclipse;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.internal.compiler.Compiler;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.ProcessHandlersASTNodeVisitor;
import jxtend.processor.eclipse.visitor.UndoAnnotationResolveASTNodeVisitor;
import jxtend.processor.eclipse.visitor.handler.ASTNodeVisitorHandler;
import jxtend.processor.eclipse.visitor.replace.ASTNodeReplaceHandler;
import jxtend.processor.eclipse.visitor.traverser.ASTNodeChildrenTraverser;
import jxtend.processor.reflect.ReflectField;

import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.apt.dispatch.BaseProcessingEnvImpl;
import org.eclipse.jdt.internal.compiler.apt.dispatch.BatchProcessingEnvImpl;
import org.eclipse.jdt.internal.compiler.apt.dispatch.RoundEnvImpl;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.impl.ReferenceContext;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReasons;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
import org.eclipse.jdt.internal.compiler.util.CompoundNameVector;
import org.eclipse.jdt.internal.compiler.util.ObjectVector;
import org.eclipse.jdt.internal.compiler.util.SimpleNameVector;
import org.eclipse.jdt.internal.core.builder.AbstractImageBuilder;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SuppressWarnings("restriction")
public final class JxtendProcessor extends javax.annotation.processing.AbstractProcessor {

	private static final ReflectField<RoundEnvImpl, CompilationUnitDeclaration[]> unitsOfRoundEnvImpl =
		ReflectField.getField(RoundEnvImpl.class, CompilationUnitDeclaration[].class, "_units");
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private static final ReflectField<CompilationResult, Map<IProblem, ReferenceContext>> problemsMapOfCompilationResult =
			(ReflectField) ReflectField.getField(CompilationResult.class, Map.class, "problemsMap");

	private static final ReflectField<CompilationUnitScope, CompoundNameVector> qualifiedReferencesOfCompilationUnitScope =
			ReflectField.getField(CompilationUnitScope.class, CompoundNameVector.class, "qualifiedReferences");
	private static final ReflectField<CompoundNameVector, Integer>  maxSizeOfCompoundNameVector =
			ReflectField.getField(CompoundNameVector.class, Integer.TYPE, "maxSize");
	private static final ReflectField<CompoundNameVector, char[][][]>  elementsOfCompoundNameVector =
			ReflectField.getField(CompoundNameVector.class, char[][][].class, "elements");

	private static final ReflectField<CompilationUnitScope, SimpleNameVector> simpleNameReferencesOfCompilationUnitScope =
			ReflectField.getField(CompilationUnitScope.class, SimpleNameVector.class, "simpleNameReferences");

	private static final ReflectField<CompilationUnitScope, SimpleNameVector> rootReferencesOfCompilationUnitScope =
			ReflectField.getField(CompilationUnitScope.class, SimpleNameVector.class, "rootReferences");

	private static final ReflectField<SimpleNameVector, Integer>  maxSizeOfSimpleNameVector =
			ReflectField.getField(SimpleNameVector.class, Integer.TYPE, "maxSize");
	private static final ReflectField<SimpleNameVector, char[][]>  elementsOfSimpleNameVector =
			ReflectField.getField(SimpleNameVector.class, char[][].class, "elements");

	private static final ReflectField<CompilationUnitScope, ObjectVector> referencedTypesOfCompilationUnitScope =
			ReflectField.getField(CompilationUnitScope.class, ObjectVector.class, "referencedTypes");

	private static final ReflectField<CompilationUnitScope, ObjectVector> referencedSuperTypesOfCompilationUnitScope =
			ReflectField.getField(CompilationUnitScope.class, ObjectVector.class, "referencedSuperTypes");

	private static final ReflectField<ObjectVector, Integer>  maxSizeOfObjectVector =
			ReflectField.getField(ObjectVector.class, Integer.TYPE, "maxSize");
	private static final ReflectField<ObjectVector, Object[]>  elementsOfObjectVector =
			ReflectField.getField(ObjectVector.class, Object[].class, "elements");
	
    private List<ASTNodeVisitorHandler> astNodeHandlers;
    private List<ASTNodeReplaceHandler> astNodeReplaceHandlers;
    private List<ASTNodeChildrenTraverser> astNodeChildrenTraversers;

	@Override
	public synchronized void init(ProcessingEnvironment processingEnv) {
		super.init(processingEnv);

	    Injector injector = Guice.createInjector(new BuiltInHandlerModule());

	    astNodeHandlers = injector.getInstance(Key.get(new TypeLiteral<List<ASTNodeVisitorHandler>>() { }));
	    astNodeReplaceHandlers = injector.getInstance(Key.get(new TypeLiteral<List<ASTNodeReplaceHandler>>() { }));
	    astNodeChildrenTraversers =
	    	injector.getInstance(Key.get(new TypeLiteral<List<ASTNodeChildrenTraverser>>() { }));
	}

	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//		System.out.println("!!!! 1");

		if (isProcessingOver(roundEnv)) {
			return false;
		}

		CompilationUnitDeclaration[] compilationUnits = getCompilationUnits(roundEnv); 

//		for (CompilationUnitDeclaration compilationUnit : compilationUnits) {
//			fixParametersOfMethodBindings(compilationUnit);
//		}

		if (processingEnv.getClass().getName().equals(
					"org.eclipse.jdt.internal.apt.pluggable.core.dispatch.IdeBuildProcessingEnvImpl")
				|| processingEnv instanceof BatchProcessingEnvImpl) {
			for (CompilationUnitDeclaration compilationUnit : compilationUnits) {
				BaseProcessingEnvImpl baseProcessingEnv = (BaseProcessingEnvImpl) processingEnv; 
				baseProcessingEnv.getCompiler().parser.getMethodBodies(compilationUnit);
			}
		}

//		System.out.println("!!!! 2");
		
		LookupEnvironment lookupEnvironment = ((BaseProcessingEnvImpl) processingEnv).getLookupEnvironment();

		if (lookupEnvironment.globalOptions == null) {
			lookupEnvironment.globalOptions = new CompilerOptions();
		}
//		setReportMissingOverrideAnnotationInCompilerOptions(lookupEnvironment.globalOptions);
//
//		if (processingEnv instanceof BatchProcessingEnvImpl) {
//			Compiler compiler = ((BatchProcessingEnvImpl) processingEnv).getCompiler();
//			setGenerateClassFilesInCompilerOptions(compiler.options);
//		}

//		for (CompilationUnitDeclaration compilationUnit : compilationUnits) {
//			setReportMissingOverrideAnnotationInCompilerOptions(compilationUnit.problemReporter.options);
//		}


//		Map<CompilationUnitDeclaration, Map<Scope, Scope>> compilationUnit2backuppedScopes =
//			new HashMap<CompilationUnitDeclaration, Map<Scope, Scope>>();
//		Map<CompilationUnitDeclaration, Map<NameReference, NameReference>> compilationUnit2backuppedNameReferences =
//			new HashMap<CompilationUnitDeclaration, Map<NameReference, NameReference>>();
		

		for (CompilationUnitDeclaration compilationUnit : compilationUnits) {
//			compilationUnit2backuppedScopes.put(
//					compilationUnit, ScopeUtils.backupBlockScopes(compilationUnit));
//			compilationUnit2backuppedNameReferences.put(
//					compilationUnit, NameReferenceUtils.backupNameReferences(compilationUnit));

			compilationUnit.problemReporter = new JxProblemReporter(compilationUnit.problemReporter);

			Set<ReferenceContext> referenceContextsWithIgnoreFurtherInvestigation = new HashSet<ReferenceContext>(); 

			Map<IProblem, ReferenceContext> problemsMap =
					problemsMapOfCompilationResult.get(compilationUnit.compilationResult);
			if ((compilationUnit.compilationResult != null) && (problemsMap != null)) {
				for (ReferenceContext referenceContext : problemsMap.values()) {
					@SuppressWarnings({ "unchecked", "rawtypes" }) ReflectField<ReferenceContext, Boolean>
						ignoreFurtherInvestigationField = (ReflectField) ReflectField.findPublicField(
								referenceContext.getClass(), Boolean.TYPE, "ignoreFurtherInvestigation");

					if ((ignoreFurtherInvestigationField != null)
							&& Boolean.TRUE.equals(ignoreFurtherInvestigationField.get(referenceContext))) {
						ignoreFurtherInvestigationField.set(referenceContext, false);
						referenceContextsWithIgnoreFurtherInvestigation.add(referenceContext);
					}
				}
			}

//			System.out.println("!!!! 3--");
			
			long start = System.currentTimeMillis();
			
//			for (TypeDeclaration typeDeclaration : compilationUnit.types) {
//				System.out.println("@@@ typeDeclaration name: " + new String(typeDeclaration.name));
//			}
			
			compilationUnit.traverse(ProcessHandlersASTNodeVisitor.create(astNodeHandlers, astNodeReplaceHandlers,
						astNodeChildrenTraversers, compilationUnit, referenceContextsWithIgnoreFurtherInvestigation),
					compilationUnit.scope);

//			System.out.println("Seconds [compilationUnit.traverse]: " + (System.currentTimeMillis() - start) / 1000D);

			long start2 = System.currentTimeMillis();
			
			problemsMap = problemsMapOfCompilationResult.get(compilationUnit.compilationResult);
			if ((compilationUnit.compilationResult != null) && (problemsMap != null)) {
				for (ReferenceContext referenceContext : referenceContextsWithIgnoreFurtherInvestigation) {
					Set<IProblem> errorsForReferenceContext = new HashSet<IProblem>();
	
					for (Map.Entry<IProblem, ReferenceContext> problemsMapEntry : problemsMap.entrySet()) {
						IProblem currentProblem = problemsMapEntry.getKey();
						
						if ((problemsMapEntry.getValue() != referenceContext) || !currentProblem.isError()) {
							continue;
						}
	
						errorsForReferenceContext.add(currentProblem);
					}

					ReflectField<CompilationResult, Integer> numberOfErrorsField =
							(ReflectField<CompilationResult, Integer>) ReflectField.getField(
									CompilationResult.class, Integer.TYPE, "numberOfErrors");
					numberOfErrorsField.set(compilationUnit.compilationResult, errorsForReferenceContext.size());

					if (!errorsForReferenceContext.isEmpty()) {
						@SuppressWarnings("unchecked")
						ReflectField<ReferenceContext, Boolean> ignoreFurtherInvestigationField =
							(ReflectField<ReferenceContext, Boolean>) ReflectField.getPublicField(
									referenceContext.getClass(), Boolean.TYPE, "ignoreFurtherInvestigation");
						ignoreFurtherInvestigationField.set(referenceContext, Boolean.TRUE);
					}
				}
			}

//			System.out.println("Seconds [POST]: " + (System.currentTimeMillis() - start2) / 1000D);
		}

//		for (Map.Entry<CompilationUnitDeclaration, Map<Scope, Scope>> entry
//				: compilationUnit2backuppedScopes.entrySet()) {
//			CompilationUnitDeclaration compilationUnit = entry.getKey();
//			Map<Scope, Scope> backuppedBlockScopes = entry.getValue(); 
//
//			ScopeUtils.restoreBlockScopes(compilationUnit, backuppedBlockScopes);
//		}
//
//		for (Map.Entry<CompilationUnitDeclaration, Map<NameReference, NameReference>> entry
//				: compilationUnit2backuppedNameReferences.entrySet()) {
//			CompilationUnitDeclaration compilationUnit = entry.getKey();
//			Map<NameReference, NameReference> backuppedNameReferences = entry.getValue(); 
//
//			NameReferenceUtils.restoreNameReferences(compilationUnit, backuppedNameReferences);
//		}		

		for (CompilationUnitDeclaration compilationUnit : compilationUnits) {
			compilationUnit.problemReporter =
				((JxProblemReporter) compilationUnit.problemReporter).originalProblemReporter;

//			if (compilationUnit.scope != null) {
//				CompoundNameVector qualifiedReferences =
//						qualifiedReferencesOfCompilationUnitScope.get(compilationUnit.scope);
//
//				CompoundNameVector filteredQualifiedReferences = new CompoundNameVector();
//				filteredQualifiedReferences.size = 0;
//				int maxSizeForFilteredQualifiedReferences =
//						qualifiedReferences.size == 0 ? 10 : qualifiedReferences.size;
//				maxSizeOfCompoundNameVector.set(filteredQualifiedReferences, maxSizeForFilteredQualifiedReferences);
//				elementsOfCompoundNameVector.set(
//						filteredQualifiedReferences, new char[maxSizeForFilteredQualifiedReferences][][]);
//
//				for (int i = 0; i < qualifiedReferences.size; i++) {
//					char[][]  qualifiedReference = qualifiedReferences.elementAt(i);
//
//					if (qualifiedReference != null) {
//						filteredQualifiedReferences.add(qualifiedReference);						
//					}
//				}
//
//				qualifiedReferencesOfCompilationUnitScope.set(compilationUnit.scope, filteredQualifiedReferences);
//
//
//				SimpleNameVector simpleNameReferences =
//						simpleNameReferencesOfCompilationUnitScope.get(compilationUnit.scope);
//
//				SimpleNameVector filteredSimpleNameReferences = new SimpleNameVector();
//				filteredSimpleNameReferences.size = 0;
//				int maxSizeForFilteredSimpleNameReferences =
//						simpleNameReferences.size == 0 ? 10 : simpleNameReferences.size;
//				maxSizeOfSimpleNameVector.set(filteredSimpleNameReferences, maxSizeForFilteredSimpleNameReferences);
//				elementsOfSimpleNameVector.set(
//						filteredSimpleNameReferences, new char[maxSizeForFilteredSimpleNameReferences][]);
//
//				for (int i = 0; i < simpleNameReferences.size; i++) {
//					char[] simpleNameReference = simpleNameReferences.elementAt(i);
//
//					if (simpleNameReference != null) {
//						filteredSimpleNameReferences.add(simpleNameReference);						
//					}
//				}
//
//				simpleNameReferencesOfCompilationUnitScope.set(compilationUnit.scope, filteredSimpleNameReferences);
//
//
//				SimpleNameVector rootReferences = rootReferencesOfCompilationUnitScope.get(compilationUnit.scope);
//
//				SimpleNameVector filteredRootReferences = new SimpleNameVector();
//				filteredRootReferences.size = 0;
//				int maxSizeForRootReferences = rootReferences.size == 0 ? 10 : rootReferences.size;
//				maxSizeOfSimpleNameVector.set(filteredRootReferences, maxSizeForRootReferences);
//				elementsOfSimpleNameVector.set(filteredRootReferences, new char[maxSizeForRootReferences][]);
//
//				for (int i = 0; i < rootReferences.size; i++) {
//					char[] rootReference = rootReferences.elementAt(i);
//
//					if (rootReference != null) {
//						filteredRootReferences.add(rootReference);						
//					}
//				}
//
//				rootReferencesOfCompilationUnitScope.set(compilationUnit.scope, filteredRootReferences);
//
//
//				ObjectVector referencedTypes = referencedTypesOfCompilationUnitScope.get(compilationUnit.scope);
//
//				ObjectVector filteredReferencedTypes = new ObjectVector(0);
//				int maxSizeForReferencedTypes = referencedTypes.size == 0 ? 10 : referencedTypes.size;
//				maxSizeOfObjectVector.set(filteredReferencedTypes, maxSizeForReferencedTypes);
//				elementsOfObjectVector.set(filteredReferencedTypes, new Object[maxSizeForReferencedTypes]);
//
//				for (int i = 0; i < referencedTypes.size; i++) {
//					Object referencedType = referencedTypes.elementAt(i);
//
//					if (referencedType != null) {
//						filteredReferencedTypes.add(referencedType);						
//					}
//				}
//
//				referencedTypesOfCompilationUnitScope.set(compilationUnit.scope, filteredReferencedTypes);
//
//
//				ObjectVector referencedSuperTypes =
//						referencedSuperTypesOfCompilationUnitScope.get(compilationUnit.scope);
//
//				ObjectVector filteredReferencedSuperTypes = new ObjectVector(0);
//				int maxSizeForReferencedSuperTypes = referencedSuperTypes.size == 0 ? 10 : referencedSuperTypes.size;
//				maxSizeOfObjectVector.set(filteredReferencedSuperTypes, maxSizeForReferencedSuperTypes);
//				elementsOfObjectVector.set(filteredReferencedSuperTypes, new Object[maxSizeForReferencedSuperTypes]);
//
//				for (int i = 0; i < referencedSuperTypes.size; i++) {
//					Object referencedSuperType = referencedSuperTypes.elementAt(i);
//
//					if (referencedSuperType != null) {
//						filteredReferencedSuperTypes.add(referencedSuperType);						
//					}
//				}
//
//				referencedSuperTypesOfCompilationUnitScope.set(compilationUnit.scope, filteredReferencedSuperTypes);
//			}

		}

//		if (processingEnv instanceof IdeBuildProcessingEnvImpl) {
//			EclipseProcessorUtils.updateCompilerEnvironment(lookupEnvironment, compilationUnits);
//		}

		for (CompilationUnitDeclaration compilationUnit : compilationUnits) {
			ASTVisitor astVistor = UndoAnnotationResolveASTNodeVisitor.createASTVisitor(new ArrayList<ASTNode>());
			compilationUnit.traverse(astVistor, compilationUnit.scope);
		}

		return true;
	}

	
	
	private void fixParametersOfMethodBindings(CompilationUnitDeclaration compilationUnit) {
		if (compilationUnit.types == null) {
			return;
		}

		for (TypeDeclaration typeDeclaration : compilationUnit.types) {
			if (typeDeclaration.methods == null) {
				continue;
			}

			for (AbstractMethodDeclaration method : typeDeclaration.methods) {
				if ((method.binding == null) || (method.arguments == null) || (method.arguments.length == 0)) {
					continue;
				}

				method.binding.parameters = new TypeBinding[method.arguments.length];
				for (int i = 0; i < method.arguments.length; i++) {
					TypeBinding argumentTypeBinding = (method.arguments[i].binding != null)
						? method.arguments[i].binding.type : null;
					
					if (argumentTypeBinding == null) {
						argumentTypeBinding = new ProblemReferenceBinding(
								TypeConstants.JAVA_LANG_OBJECT, null, ProblemReasons.NotFound);
					}

					method.binding.parameters[i] = argumentTypeBinding;
				}
			}
		}
	}


	protected boolean isProcessingOver(RoundEnvironment roundEnv) {
		return roundEnv.processingOver() && isImageBuilderCompilation();
	}

	private boolean isImageBuilderCompilation() {
		org.eclipse.jdt.internal.compiler.Compiler compiler = ((BaseProcessingEnvImpl) processingEnv).getCompiler();
		return (compiler != null) && (compiler.requestor instanceof AbstractImageBuilder
				|| compiler.requestor.getClass().getName().startsWith("org.eclipse.jdt.internal.compiler.batch.Main$"));
	}

	private CompilationUnitDeclaration[] getCompilationUnits(RoundEnvironment roundEnv) {
		CompilationUnitDeclaration[] compilationUnits = unitsOfRoundEnvImpl.get((RoundEnvImpl) roundEnv);

		if (compilationUnits != null) {
			return compilationUnits;
		}

		compilationUnits = ((BaseProcessingEnvImpl) processingEnv).getCompiler().unitsToProcess;
		return (compilationUnits == null) ? new CompilationUnitDeclaration[0] : compilationUnits;
	}
	
	private static void setReportMissingOverrideAnnotationInCompilerOptions(CompilerOptions compilerOptions) {
		@SuppressWarnings("unchecked") Map<String, String> optionsMap = compilerOptions.getMap();
		optionsMap.put(CompilerOptions.OPTION_ReportMissingOverrideAnnotation, CompilerOptions.ERROR);
		optionsMap.put(CompilerOptions.OPTION_ReportMissingOverrideAnnotationForInterfaceMethodImplementation,
				CompilerOptions.ENABLED);
		compilerOptions.set(optionsMap);
	}

	private static void setGenerateClassFilesInCompilerOptions(CompilerOptions compilerOptions) {
		@SuppressWarnings("unchecked") Map<String, String> optionsMap = compilerOptions.getMap();
		optionsMap.put(CompilerOptions.OPTION_GenerateClassFiles, CompilerOptions.ENABLED);
		compilerOptions.set(optionsMap);
	}
	
	private static final class JxProblemReporter extends ProblemReporter {
		private ProblemReporter originalProblemReporter;

		public JxProblemReporter(ProblemReporter originalProblemReporter) {
			super(originalProblemReporter.policy, originalProblemReporter.options,
					originalProblemReporter.problemFactory);
			
			this.originalProblemReporter = originalProblemReporter;
		}

		public void handle(int problemId, String[] problemArguments, int elaborationId, String[] messageArguments,
				int severity, int problemStartPosition, int problemEndPosition, ReferenceContext referenceContext,
				CompilationResult unitResult) {
			if ((problemId == IProblem.UnresolvedVariable) || (problemId == IProblem.UndefinedName)
					|| (problemId == IProblem.UndefinedMethod)) {
				// Do nothing
			} else {
				super.handle(problemId, problemArguments, elaborationId, messageArguments, severity,
						problemStartPosition, problemEndPosition, referenceContext, unitResult);
			}
		}

		public void record(
				CategorizedProblem problem, CompilationResult unitResult, ReferenceContext referenceContext) {
			originalProblemReporter.record(problem, unitResult, referenceContext);
		}
	}	
}
